<?php
/**
 * @Author: Marte
 * @Date:   2020-05-19 17:51:21
 * @Last Modified by:   Marte
 * @Last Modified time: 2020-05-22 19:31:18
 */
namespace Framework;

class Session
{
    private $sessionId, $redisCache, $userIp;

    public static $SESSION_TABLE_NAME = 'SESSION_TABLE_NAME';

    public static $SESSION_NAME = 'SESSION_NAME';

    public static $SESSION_TIMEOUT = 1800;

    public static $COOKIE_PATH = '/';

    public static $COOKIE_DOMAIN = '';


    public function __construct($options = [])
    {
        foreach($options as $k => $option){
            self::${$k} = $option;
        }

        if(!isset($options['SESSION_TIMEOUT']) || $options['SESSION_TIMEOUT'] <= 0){
            self::$SESSION_TIMEOUT = ini_get('session.gc_maxlifetime');
        }
    }

    public function start()
    {
        //注册Session的各种处理函数
        session_set_save_handler(
            [__CLASS__,  'open'],      // 在运行session_start()时执行
            [__CLASS__,  'close'],     // 在脚本执行完成 或 调用session_write_close() 或 session_destroy()时被执行，即在所有session操作完后被执行
            [__CLASS__,  'read'],      // 在运行session_start()时执行，因为在session_start时，会去read当前session数据
            [__CLASS__,  'write'],     // 此方法在脚本结束和使用session_write_close()强制提交SESSION数据时执行
            [__CLASS__,  'destroy'],   // 在运行session_destroy()时执行
            [__CLASS__,  'gc']         // 执行概率由session.gc_probability 和 session.gc_divisor的值决定，时机是在open，read之后，session_start会相继执行open，read和gc
        );
        session_start(); // 这也是必须的，打开session，必须在session_set_save_handler后面执行
    }

    /**
     * session_start时会调用该函数
     * @param $savePath 默认的保存路径
     * @param $sessionName 默认的参数名，PHPSESSID
     * @return bool
     */
    public function open($savePath, $sessionName)
    {
        //生成或获取一个session id
        $this->get_sid();
        //获取用于存储Session的Redis对象实例
        $this->redisCache = new \Redis();
        $this->redisCache->connect('127.0.0.1',6379);
        return true;
    }

    /**
     * 使用SessionId作为key，从Redis中读取相应数据
     * @return string
     */
    public function read($sessionId)
    {
        $sessionValue = $this->redisCache->get(self::$SESSION_TABLE_NAME . $sessionId);
        if(empty($sessionValue)){
            return '';
        }else{
            return $sessionValue;
        }
    }

    /**
     * 将Session变量的内容写入Redis中
     * @param $sessionId 要写入数据的session对应的id
     * @param $sessionData 要写入的数据，已经序列化过了
     * @return bool
     */
    public function write($sessionId, $sessionData)
    {
        // 读取$_SESSION后都会调用此方法,要避免过期时间被重置
        // $this->redisCache->setex(self::$SESSION_TABLE_NAME . $sessionId, self::$SESSION_TIMEOUT, $sessionData);
        // var_dump($this->redisCache->ttl(self::$SESSION_TABLE_NAME . $sessionId));

        $sessionValue = $this->redisCache->get(self::$SESSION_TABLE_NAME . $sessionId);
        if(empty($sessionValue) || $sessionData != $sessionValue){
            $this->redisCache->setex(self::$SESSION_TABLE_NAME . $sessionId, self::$SESSION_TIMEOUT, $sessionData);
        }
        return true;
    }

    /**
     * 通过删除Redis中SessionId对应的数据来注销Session
     * session_destroy()是自动调用
     * @param $sessionId 要销毁的会话的唯一id
     * @return bool
     */
    public function destroy($sessionId)
    {
        $this->redisCache->delete(self::$SESSION_TABLE_NAME . $sessionId);
        setcookie(self::$SESSION_NAME, $sessionId, 1, self::$COOKIE_PATH, self::$COOKIE_DOMAIN, FALSE);
        return true;
    }

    public function close()
    {
        $this->redisCache->close();
        // echo $this->print_stack_trace();
        return true;
    }

    /**
     * 清理会话中的过期数据
     * @param 有效期
     */
    public function gc($lifeTime)
    {
        return true;
    }

    public function __destruct(){
        session_write_close();
    }

    /**
     * 返回一个SessionId
     * 若Cookie中已存在SessionId，则直接返回该SessionId
     * 若不存在，则按照规则新生成一个SessionId
     *
     * @return string Session Id
     */
    public function get_sid()
    {
        $this->userIp = $this->get_real_ip();
        //判断Cookie中是否已经存在SessionId
        if(empty($_COOKIE[self::$SESSION_NAME])) {
            //使用MD5对用户IP+随机字符串加密后作为新的SessionId
            $sessionId = md5($this->userIp . uniqid(mt_rand(), true));
            //对新的SessionId再做一次crc32运算，作为最终的SessionId
            $sessionId .= sprintf('%08x', crc32($sessionId));
            //将SessionId写入Cookie中
            setcookie(self::$SESSION_NAME, $sessionId, time() + self::$SESSION_TIMEOUT, self::$COOKIE_PATH, self::$COOKIE_DOMAIN, FALSE);
            $_COOKIE[self::$SESSION_NAME] = $sessionId;
        }else{
        	$sessionId = $_COOKIE[self::$SESSION_NAME];
        }
        //返回SessionId
        return $sessionId;
    }

    /**
     * 获取客户端IP
     * @return [type] [description]
     */
    public function get_real_ip(){
        return '127.0.0.1';
    }

    //打印堆栈信息
    public function print_stack_trace()
    {
        $array = debug_backtrace();
        unset($array[0]);   //去掉当前方法
        $traceInfo = '<br />++++++++++++++++<br />';
        $time = date ( "y-m-d H:i:m" );
        foreach ($array as $t) {
            if(isset($t['file'])){
                $traceInfo .= '[' . $time . '] ' . $t ['file'] . ' (' . $t ['line'] . ') ';
                $traceInfo .= $t ['function'] . '(';
                $traceInfo .= implode ( ', ', $t ['args'] );
                $traceInfo .= ")<br />";
            }else{
                $traceInfo .= '[' . $time . '] ';
                $traceInfo .= $t ['class'] . $t ['type'] . $t ['function'] . '(';
                $traceInfo .= implode ( ', ', $t ['args'] );
                $traceInfo .= ")<br />";
            }
        }
        $traceInfo .= '++++++++++++++++';
        return $traceInfo;
    }

}

$session = new Session(['SESSION_TIMEOUT' => 25]);
$session->start();

//第一次访问
$_SESSION['name'] = 'heihei';
echo $_SESSION['name'];


//第二次访问,测试能否修改session
// $_SESSION['name'] = 'hahha';
// echo $_SESSION['name'] ?? 'session不存在';


// 第三次访问时,$_SESSION['name']等于hahha,session设置成功
// echo $_SESSION['name'] ?? 'session不存在';